package com.tomato.modules.id.strategy.impl;

import com.google.common.base.Preconditions;
import com.tomato.modules.id.strategy.IdGeneratorStrategyService;
import lombok.Setter;
import lombok.SneakyThrows;

/**
 * 雪花算法生成id
 *
 * @author lizhifu
 * @date 2022/4/6
 */
public class SnowFlakeGenerateStrategy implements IdGeneratorStrategyService {
    @Setter
    private static TimeService timeService = new TimeService();
    /**
     * sequence 偏移量
     */
    private int sequenceOffset = 0;
    /**
     * ID中41位时间戳的起点
     * 1649221377747 2022年04月06日13:03:28
     */
    public static final long EPOCH = 1649221377747L;
    /** 允许的最大时间差 10ms **/
    private static final long MAX_TIME_DIFF = 10L;
    /**
     * 最后12位，代表每毫秒内可产生最大序列号，即 2^12 - 1 = 4095
     */
    private static final long SEQUENCE_BITS = 12L;
    /**
     * 同一毫秒内最新序号，最大正整数可为 2^12 - 1 = 4095
     */
    private long sequence;
    /**
     * 掩码（最低12位为1，高位都为0），主要用于与自增后的序列号进行位与，如果值为0，则代表自增后的序列号超过了4095
     * 0000000000000000000000000000000000000000000000000000111111111111
     */
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    /**
     * 上次使用的时间戳，单位毫秒
     */
    private long lastMilliseconds;
    /**
     * 工作id占用的位数
     */
    private static final long WORKER_ID_BITS = 10L;
    /**
     * 工作机器ID,占用5位，最大值为31
     */
    private long workerId;
    /**
     * workId占用5个比特位，最大值31
     * 0000000000000000000000000000000000000000000000000000000000011111
     */
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

    /**
     * workId位需要左移的位数 12
     */
    private static final long WORK_ID_SHIFT = SEQUENCE_BITS;

    /**
     * 时间戳左移的位数，时间戳占用的位数
     * 12+10=22
     */
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    public SnowFlakeGenerateStrategy(long workerId,int sequenceOffset) {
        // 检查workId的合法值
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException(String.format("workId值必须大于0并且小于%d", MAX_WORKER_ID));
        }
        this.workerId = workerId;

        if (sequenceOffset <0){
            sequenceOffset = 0;
        }
        this.sequenceOffset = sequenceOffset;
    }

    @Override
    public synchronized Long nextId() {
        long currentMilliseconds = timeService.getCurrentTime();
        if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
            currentMilliseconds = timeService.getCurrentTime();
        }
        // 如果当前时间和上次时间相等，则需要生成新的sequence
        if (lastMilliseconds == currentMilliseconds) {
            // 序列号的最大值是4095，使用掩码（最低12位为1，高位都为0）进行位与运行后如果值为0，则自增后的序列号超过了4095
            // 那么就使用新的时间戳
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                // 如果序列号超过了4095，则需要等待时钟恢复
                currentMilliseconds = waitUntilNextTime(lastMilliseconds);
            }
        }else {
            // 如果当前时间和上次时间不相等，则需要重置sequence
            // 为了保证 id 不全在低位区
            sequence = sequenceOffset;
        }
        // 上次使用的时间戳 = 当前时间戳
        lastMilliseconds = currentMilliseconds;
        // 核心算法，将不同部分的数值移动到指定的位置，然后进行或运行
        return ((currentMilliseconds - EPOCH) << TIMESTAMP_SHIFT) | (workerId << WORK_ID_SHIFT) | sequence;
    }

    /**
     * 时钟回退
     * @param currentMilliseconds
     * @return
     */
    @SneakyThrows(InterruptedException.class)
    private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
        // 如果当前时间小于上次时间，则说明时钟回退了，这时需要等待时钟恢复后再继续生成id
        if (lastMilliseconds <= currentMilliseconds) {
            return false;
        }
        // 如果时钟回退了，则需要等待时钟恢复的时间为当前时间减去上次时间
        long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
        Preconditions.checkState(timeDifferenceMilliseconds < MAX_TIME_DIFF,
                "时钟回退了，请等待时钟恢复, 上次时间 %d ms, 当前时间 %d ms", lastMilliseconds, currentMilliseconds);
        // 等待时钟恢复
        Thread.sleep(timeDifferenceMilliseconds);
        return true;
    }

    /**
     * 获取下一秒
     * @param lastTime
     * @return
     */
    private long waitUntilNextTime(final long lastTime) {
        long result = timeService.getCurrentTime();
        while (result <= lastTime) {
            result = timeService.getCurrentTime();
        }
        return result;
    }
}
